package com.game;

import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import org.apache.log4j.Logger;

import com.common.game.Bid;
import com.common.game.Dice;
import com.common.game.Player;
import com.common.utilities.SendData;
import com.server.NioServer;

/**
 * User: wattsc5
 * <p/>
 * Date: 2/27/12
 */
public class ServerGame
{
	private static final Logger logger = Logger.getLogger(ServerGame.class);

	private final Map<Integer, Player> playersInServer = new HashMap<Integer, Player>();

	private ServerGameSettings settings;

	private ServerGameWorker gameWorker;

	private int playerIdCount;

	private int playersInGameCount;

	private Timer gameLobbyTimer;

	private boolean hasTimerStarted;

	private GameState gameState;

	private Player playersTurn;

	private Player lastPlayerToJoin;

	private Player lastPlayersTurn;

	private boolean hasGameStarted;

	private Bid lastBid;

	/**
	 * Initializes a new instance of a ServerGame.
	 *
	 * @param gameSettings the settings for the game.
	 */
	public ServerGame(ServerGameSettings gameSettings)
	{
		this.settings = gameSettings;
	}

	public ServerGameWorker getGameWorker()
	{
		return gameWorker;
	}

	public void initializeGame(ServerGameAPI api, int port)
	{
		this.gameWorker = new ServerGameWorker();
		this.gameLobbyTimer = new Timer();

		NioServer server = new NioServer(port);

		// Set up the game worker.
		this.getGameWorker().setApi(api);
		this.getGameWorker().setServer(server);

		// Set up the server.
		server.setWorker(this.getGameWorker());

		new Thread(this.getGameWorker()).start();
		new Thread(server).start();

		this.changeState(GameState.lobby);
	}

	public Player addPlayer(SocketChannel socketChannel, String playerName)
	{
		Player player = null;

		if (this.playersInServer.size() < this.settings.getMaxNumberOfPlayers())
		{
			player = new Player(playerName, playerIdCount);

			if (this.hasGameStarted)
			{
				player.setInGame(false);
			}
			else
			{
				playersInGameCount++;
				player.setInGame(true);
			}
			player.setSocketChannel(socketChannel);
			player.addDie(this.getSettings().getStartingNumberOfDie());

			this.playersInServer.put(playerIdCount, player);
			playerIdCount++;
		}
		else
		{
//			SendData data = new SendData();
//			data.addArgument("error");
//			data.addArgument("server full");
//			this.sendMessageToSocket(socketChannel, data);
			logger.info("Player tried to join but server is full - " + playerName);
		}

		if (playersInGameCount == this.settings.getMinNumberOfPlayers() &&
			this.gameState == GameState.lobby)
		{
			this.changeState(GameState.lobby_with_timer);
		}

		return player;
	}

	private void changeState(GameState state)
	{
		this.gameState = state;
		switch (state)
		{
			case in_game:
				logger.info("Entering game state");
				this.hasGameStarted = true;
				break;
			case lobby:
				this.hasGameStarted = false;
				logger.info("Entering lobby state");
				this.gameLobbyTimer.cancel();
				this.hasTimerStarted = false;
				this.sendStateToAllPlayers();

				this.playersInGameCount = this.playersInServer.size();
				this.lastPlayersTurn = null;

				for (Player player : this.playersInServer.values())
				{
					player.setInGame(true);
					player.setNumberOfDie(this.getSettings().getStartingNumberOfDie());
				}

				if (this.playersInGameCount >= this.getSettings().getMinNumberOfPlayers())
				{
					logger.trace("We have enough players to go right from the lobby state to the lobby with timer state");
					this.changeState(GameState.lobby_with_timer);
				}

				break;
			case lobby_with_timer:
				this.hasGameStarted = false;
				logger.info("Entering lobby with timer state");
				SendData data = new SendData();
				data.addArgument("timer_start");
				data.addArgument(settings.getWaitTimeToStart());
				this.sendMessageToAllPlayers(data);
				logger.info("Starting lobby timer");

				this.hasTimerStarted = true;
				this.gameLobbyTimer = new Timer();
				this.gameLobbyTimer.schedule(new TimerTask()
				{
					@Override public void run()
					{
						startRound();
					}
				}, this.settings.getWaitTimeToStart() * 1000);

				break;
		}
	}

	public void playerQuit(SocketChannel channel)
	{
		Player player = this.getPlayerBySocket(channel);

		if (player == null)
		{
			logger.warn("Player could not be found from socket");
			return;
		}

		player.cancelTimeoutTimer();

		this.playersInServer.remove(player.getId());
		if (this.playersInServer.size() != 0)
		{
			SendData sendData = new SendData();
			sendData.addArgument("client_quit");
			sendData.addArgument(player.getId());
			this.sendMessageToAllPlayers(sendData);
		}

		logger.info("Player quit: " + player);

		this.removePlayerFromServer(player);

		if (this.playersInServer.size() == 1)
		{
			this.changeState(GameState.lobby);
		}
	}

	public void kickPlayer(Player player)
	{
		if (player == null)
		{
			logger.error("Cannot kick a null player");
			return;
		}

		SendData sendData = new SendData();
		sendData.addArgument("client_kicked");
		sendData.addArgument(player.getId());
		this.sendMessageToAllPlayers(sendData);

		logger.info("Player was kicked : " + player);

		if (player.isInGame())
		{
			this.playersInGameCount--;
		}

		if (this.playersInGameCount == 1 &&
		    this.gameState == GameState.in_game)
		{
			logger.info("Only one player is left in the game so end the game.");
			this.endGame();
			return;
		}

		if (player == this.playersTurn)
		{
			this.changePlayersTurn(player);
			this.lastPlayersTurn = player.getPreviousPlayer();
		}
		else if (player.getNextPlayer() == this.playersTurn)
		{
			this.lastPlayersTurn = player.getPreviousPlayer();
		}

		player.cancelTimeoutTimer();
		player.setInGame(false);
	}

	public void removePlayerFromServer(Player player)
	{
		if (player == null)
		{
			logger.error("Cannot remove a null player");
			return;
		}

		if (player.isInGame())
		{
			this.playersInGameCount--;
		}

		if (this.playersInServer.size() == 0)
		{
			return;
		}

		if (this.playersInGameCount < this.settings.getMinNumberOfPlayers() &&
		    this.hasTimerStarted &&
			this.gameState == GameState.lobby_with_timer)
		{
			this.changeState(GameState.lobby);
			return;
		}
		else if (this.playersInGameCount == 1 &&
		         this.gameState == GameState.in_game)
		{
			logger.info("Only one player is left in the game so we will go back to the lobby.");
			this.endGame();
			return;
		}

		if (player == this.playersTurn)
		{
			this.changePlayersTurn(player);
			this.lastPlayersTurn = player.getPreviousPlayer();
		}
		else if (player.getNextPlayer() == this.playersTurn)
		{
			this.lastPlayersTurn = player.getPreviousPlayer();
		}

		if (player.getPreviousPlayer() != null &&
		    player.getNextPlayer() != null)
		{
			player.getPreviousPlayer().setNextPlayer(player.getNextPlayer());
			player.getNextPlayer().setPreviousPlayer(player.getPreviousPlayer());
		}

		player.cancelTimeoutTimer();

		//this.gameWorker.disconnectClient(socketChannel);
	}

	public void sendMessageToSocket(SocketChannel socketChannel, SendData sendData)
	{
		logger.trace("Sending message to socket - " + socketChannel.socket().getInetAddress() + ":" + socketChannel.socket().getPort() + " - " + sendData.toString());
		this.gameWorker.addDataToSend(socketChannel, sendData);
	}

	public void sendMessageToPlayer(Player player, SendData sendData)
	{
		this.sendMessageToSocket(player.getSocketChannel(), sendData);
	}

	public void sendMessageToAllPlayers(SendData sendData)
	{
		logger.trace("Sending message to all players - "  + sendData.toString());

		for (Player player : this.playersInServer.values())
		{
			this.sendMessageToPlayer(player, sendData);
		}
	}

	public Player getPlayerBySocket(SocketChannel socketChannel)
	{
		Player result = null;

		for (Player player : this.playersInServer.values())
		{
			if (player.getSocketChannel() == socketChannel)
			{
				result = player;
				break;
			}
		}

		return result;
	}

	public Player getPlayerByName(String name)
	{
		Player result = null;

		for (Player player : this.playersInServer.values())
		{
			if (player.getName().equals(name))
			{
				result = player;
				break;
			}
		}

		return result;
	}

	public Player getPlayerById(int id)
	{
		return this.playersInServer.get(id);
	}

	public ServerGameSettings getSettings()
	{
		return settings;
	}

	private void startRound()
	{
		this.lastBid = null;
		this.changeState(GameState.in_game);
		logger.info("Starting round");

		SendData roundStartData = new SendData();
		roundStartData.addArgument("round_start");
		roundStartData.addArgument(this.playersInGameCount);
		for (Player player : this.playersInServer.values())
		{
			if (player.isInGame())
			{
				player.rollDice();
				roundStartData.addArgument(player.getId());
				roundStartData.addArgument(Integer.toString(player.getDiceCount()));
			}
		}

		this.sendMessageToAllPlayers(roundStartData);

		for (Player player : this.playersInServer.values())
		{
			if (player.isInGame())
			{
				SendData playerDiceData = new SendData();
				playerDiceData.addArgument("your_dice");
				playerDiceData.addArgument(player.getDiceCount());

				for (Dice die : player.getDice())
				{
					playerDiceData.addArgument(die.getValue());
				}

				this.sendMessageToPlayer(player, playerDiceData);
			}
		}

		this.lastPlayersTurn = null;
		this.playersTurn = this.getNextPlayablePlayer(this.lastPlayersTurn);
		this.lastPlayerToJoin.setNextPlayer(this.playersTurn);
		this.playersTurn.setPreviousPlayer(this.lastPlayerToJoin);
		this.startPlayerTurn(this.playersTurn);
	}

	private Player getNextPlayablePlayer(Player playersTurn)
	{
		if (playersTurn == null)
		{
			return this.getNextPlayablePlayer(this.playersInServer.values().iterator().next());
		}

		if (playersTurn.isInGame())
		{
			return playersTurn;
		}

		return this.getNextPlayablePlayer(playersTurn.getNextPlayer());
	}

	private void endRound(Player playerWhoLost)
	{
		this.lastBid = null;
		SendData roundEndData = new SendData();
		roundEndData.addArgument("round_end");
		roundEndData.addArgument(playerWhoLost.getId());
		roundEndData.addArgument(this.playersInGameCount);

		for (Player player : this.playersInServer.values())
		{
			if (player.isInGame())
			{
				roundEndData.addArgument(player.getId());
				roundEndData.addArgument(player.getDiceCount());

				for (Dice die : player.getDice())
				{
					roundEndData.addArgument(die.getValue());
				}
			}
		}

		this.sendMessageToAllPlayers(roundEndData);

		// Remove dice here so that we can send all the correct info above to the client
		playerWhoLost.removeDie();
		if (playerWhoLost.getDiceCount() == 0)
		{
			playerWhoLost.setInGame(false);
			this.playersInGameCount--;
			logger.info(playerWhoLost + " lost and now is out of the game");
		}

		logger.info("Round ended");

		if (this.playersInGameCount == 1)
		{
			this.endGame();
			return;
		}

		this.startRound();
	}

	private void endGame()
	{
		this.lastBid = null;
		this.hasGameStarted = false;
		logger.info("Game ended");

		Player winner = null;
		for (Player player : this.playersInServer.values())
		{
			if (player.isInGame())
			{
				winner = player;
			}
			else
			{
				// Otherwise set the player up to start the next round.
				player.setInGame(true);
				player.addDie(this.getSettings().getStartingNumberOfDie());
			}
		}

		if (winner == null)
		{
			logger.error("Winner was null");
			return;
		}

		SendData data = new SendData();
		data.addArgument("game_end");
		data.addArgument(winner.getId());
		sendMessageToAllPlayers(data);

		winner.cancelTimeoutTimer();
		this.gameLobbyTimer.cancel();
		this.changeState(GameState.lobby);
	}

	private void invalidMove(Player player)
	{
		if (player.getNumberOfInvalidMoves() >= this.settings.getAttemptMoveCount())
		{
			logger.info("Kicking player for to many invalid moves");
			this.kickPlayer(player);
		}
		else
		{
			SendData data = new SendData();
			data.addArgument("invalid_move");
			data.addArgument(player.getNumberOfInvalidMoves());
			this.sendMessageToPlayer(player, data);
			player.addInvalidMove();
		}

		logger.info("Invalid move");
	}

	private void changePlayersTurn(Player playersTurn)
	{
		if (playersTurn == null)
		{
			logger.error("Changing player null");
			return;
		}

		if (playersTurn.getNextPlayer() == null)
		{
			logger.warn("Could not change player because there is no one to change to");
			return;
		}

		if (playersTurn.getNextPlayer().isInGame())
		{
			this.lastPlayersTurn = this.playersTurn;
			this.playersTurn = playersTurn.getNextPlayer();
			this.startPlayerTurn(this.playersTurn);
		}
		else
		{
			this.changePlayersTurn(playersTurn.getNextPlayer());
		}
	}

	private void startPlayerTurn(final Player player)
	{
		logger.info("Starting next players turn");
		player.getNewTimeoutTimer().schedule(new TimerTask()
		{
			@Override public void run()
			{
				logger.info("Player timed out");
				kickPlayer(player);
			}
		}, this.settings.getTurnTimeout() * 1000);

		this.broadcastPlayersTurn();
	}

	private void broadcastPlayersTurn()
	{
		SendData playerTurnData = new SendData();
		playerTurnData.addArgument("player_turn");
		playerTurnData.addArgument(this.playersTurn.getId());

		this.sendMessageToAllPlayers(playerTurnData);
	}

	public void join(SocketChannel socketChannel, String name)
	{
		Player player = this.addPlayer(socketChannel, name);

		if (player != null)
		{
			SendData sendData = new SendData();
			sendData.addArgument("client_joined");
			sendData.addArgument(player.getName());
			sendData.addArgument(player.getId());
			this.sendMessageToAllPlayers(sendData);

			logger.info("Player joined game - " + player.toString());

			if (this.playersTurn == null)
			{
				this.playersTurn = player;
			}
			else
			{
				this.lastPlayerToJoin.setNextPlayer(player);
				Player firstPlayer = this.playersInServer.values().iterator().next();
				firstPlayer.setPreviousPlayer(player);
				player.setNextPlayer(firstPlayer);
				player.setPreviousPlayer(this.lastPlayerToJoin);
			}

			this.lastPlayerToJoin = player;

			this.sendStateToPlayer(player.getId());
		}
	}

	public void bid(SocketChannel socketChannel, int diceCount, int diceValue)
	{
		Player player = this.getPlayerBySocket(socketChannel);

		if (player == null)
		{
			logger.warn("Cannot find player when trying to bid");
			return;
		}

		if (this.gameState != GameState.in_game)
		{
			logger.warn("Player tried to bid when the game hasn't started.");
			this.invalidMove(player);
			return;
		}

		if (!player.isInGame())
		{
			// TODO: Maybe send a message saying we are not in a game
			logger.warn("The player bidding is not in the game");
			return;
		}

		if (player != this.playersTurn)
		{
			this.invalidMove(player);
			return;
		}

		player.cancelTimeoutTimer();

		if (!this.isBidValid(player, diceCount, diceValue))
		{
			this.invalidMove(player);
			return;
		}

		Bid bid = new Bid(diceCount, diceValue);
		player.setNewBid(bid);
		this.lastBid = bid;

		SendData data = new SendData();
		data.addArgument("bid_report");
		data.addArgument(player.getId());
		data.addArgument(player.getBid().getDiceCount());
		data.addArgument(player.getBid().getDiceValue());
		this.sendMessageToAllPlayers(data);

		logger.info("Player bid");

		this.changePlayersTurn(player);
	}

	public boolean isBidValid(Player player, int bidDiceCount, int diceValue)
	{
		int playerDiceCount = player.getDiceCount();

		if ((bidDiceCount < 0 || bidDiceCount > playerDiceCount) ||
			(diceValue < 0 || diceValue > 6))
		{
			logger.warn("Bid was invalid");
			return false;
		}

		if (this.lastBid != null)
		{
			if (bidDiceCount > this.lastBid.getDiceCount())
			{
				return true;
			}
			else if (bidDiceCount >= this.lastBid.getDiceCount() && diceValue > this.lastBid.getDiceValue())
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		return true;
	}

	public void challenge(SocketChannel channel)
	{
		Player player = this.getPlayerBySocket(channel);

		if (this.lastBid == null)
		{
			logger.warn("Player tried to challenge when there hasn't been a first bid.");
			this.invalidMove(player);
			return;
		}

		if (player == null)
		{
			logger.warn("Cannot find player when trying to challenge");
			return;
		}

		if (this.gameState != GameState.in_game)
		{
			logger.warn("Player tried to bid when the game hasn't started.");
			this.invalidMove(player);
			return;
		}

		if (!player.isInGame())
		{
			logger.warn("Player trying to challenge is not in the game");
			// TODO: Maybe send a message saying we are not in a game
			this.invalidMove(player);
			return;
		}

		if (player != this.playersTurn)
		{
			this.invalidMove(player);
			return;
		}

		player.cancelTimeoutTimer();

		SendData data = new SendData();
		data.addArgument("challenge_report");
		data.addArgument(player.getId());
		this.sendMessageToAllPlayers(data);

		logger.info("Player challenged");

		int numberCorrect =  0;
		if (this.lastPlayersTurn == null)
		{
			logger.warn("No last player when challenging");
			this.invalidMove(player);
			return;
		}

		for(Dice die : this.lastPlayersTurn.getDice())
		{
			if (die.getValue() == this.lastPlayersTurn.getBid().getDiceValue())
			{
				numberCorrect++;
			}
		}

		if (numberCorrect == this.lastPlayersTurn.getBid().getDiceCount())
		{
			logger.info("Previous player did not lie");
			// Player lost challenge
			this.endRound(this.playersTurn);
		}
		else
		{
			logger.info("Previous player lied");
			// Player won challenge
			this.endRound(this.lastPlayersTurn);
		}
	}

	public void sendStateToPlayer(int playerId)
	{
		SendData data = new SendData();
		data.addArgument("state");
		data.addArgument(this.gameState.toString());
		data.addArgument(this.getPlayerById(playerId).getId());
		data.addArgument(this.playersInServer.size());

		for (Player player : this.playersInServer.values())
		{
			data.addArgument(player.getName());
			data.addArgument(player.getId());
		}

		this.sendMessageToPlayer(this.getPlayerById(playerId), data);

		logger.info("Sending state to player");
	}

	private void sendStateToAllPlayers()
	{
		for (Player player : this.playersInServer.values())
		{
			this.sendStateToPlayer(player.getId());
		}
	}
}
